﻿using System.Net;
using System.Threading;
using my_proxy.Utils;
using System.Collections.Generic;
using System;
using System.IO;
using System.Text;

namespace my_proxy.RequestHandlers
{
    /// <summary>
    /// Handler all Client's requests and deliver the web site
    /// </summary>
    public class ProxyListener
    {
        //the conf that holds all the configartion data
        readonly Configuration _conf;
        //File Reader that reads and writes to the whitelist blacklist files 
        readonly FileReader _fileReader;
        //a lock that sync the writing to the files
        readonly object _fileLock = new object();

        readonly List<string> whiteList;
        readonly List<string> blackList;
        private Dictionary<string, overLoadUsers> overLoadUser;
        private List<string> recentOverLoadUser;
        public static string message403;
        public static HttpListenerResponse preMade403Response;

        public ProxyListener(Configuration cf, FileReader fr)
        {
            this._conf = cf;
            this._fileReader = fr;
            this._fileLock = new object();
            whiteList = this._fileReader.getDecryptedList(this._conf.WhiteFile);
            blackList = this._fileReader.getDecryptedList(this._conf.BlackFile);
            overLoadUser = new Dictionary<string, overLoadUsers>();
            recentOverLoadUser = new List<string>();
        }
        /// <summary>
        /// the main function,creates a new listener on the given port and sends it contexts to the threads that 
        /// handle the requests
        /// </summary>
        public void HandleReq()
        {
            // Create a listener
            HttpListener listener = new HttpListener();
            HttpListenerBasicIdentity m_identity = null;
            // Add the prefix
            listener.Prefixes.Add("http://" + this._conf.Host + ":" + this._conf.Port + "/");
            //add the function that will check if we need to a Authenticate the request
            listener.AuthenticationSchemeSelectorDelegate = this.AuthenticationSchemeForClient;
            //start the listener
            listener.Start();

            while (true)
            {
                // Specify the authentication delegate.
                //if (whiteList.Contains(this.context.Request.RemoteEndPoint.Address.ToString()))
                HttpListenerContext context = listener.GetContext();
                OpenRequestHandler requestHandler;

                //choose the state of the proxy - Open / Anonymous
                if (null != context.User)
                    m_identity = (HttpListenerBasicIdentity)context.User.Identity;
              
                if (whiteList.Contains(context.Request.RemoteEndPoint.Address.ToString()) || (null != m_identity && m_identity.Password == _conf.Password))
                {
                    if (!overloadReq(context.Request.RemoteEndPoint.Address.ToString()))//if x packet in y
                    {
                        if (validUserNsite(context)) continue;
                        //if not forbidden site
                        switch (this._conf.Mode)
                        {
                            case 0:
                                requestHandler = new OpenRequestHandler(this._conf, context, this._fileReader, this._fileLock);
                                break;
                            default:
                                requestHandler = new AnonymousRequestHandler(this._conf, context, this._fileReader, this._fileLock);
                                break;
                        }
                        new Thread(requestHandler.ProcessRequest).Start();
                    }
                }
                else
                    forbbiden(context);
            }
        }

        private bool validUserNsite(HttpListenerContext context)
        {

            if (!whiteList.Contains(context.Request.RemoteEndPoint.Address.ToString()))
            {
                whiteList.Add(context.Request.RemoteEndPoint.Address.ToString());
                this._fileReader.addEncryptedUser(context.Request.RemoteEndPoint.Address.ToString(), _conf.WhiteFile);
            }
            //if requset for site that in black list or user ask for more then x packets in y second
            if (blackList.Contains(context.Request.RawUrl) || blackList.Contains(context.Request.Url.Host))
            {
                forbbiden(context);
                return true;
            }


            return false;
        }

        /// <summary>
        /// check if user sent more then x request in Y second
        /// </summary>
        /// <param name="userIp"></param>
        /// <returns> false if user didn't cross the limit of X packet in Y second,
        ///           else remove from white list and return true</returns>
        private bool overloadReq(string userIp)
        {
            //for first user request
            if (!overLoadUser.ContainsKey(userIp))
            {
                overLoadUser.Add(userIp, new overLoadUsers());
                return false;
            }
            //add new packet
            overLoadUser[userIp].addPacket();

            //delete request that no longer relevent to my interval
            while ((DateTime.Now - overLoadUser[userIp].getFirstTime()).TotalSeconds > _conf.Y)
            {
                overLoadUser[userIp].removePacket();
            }
            if (overLoadUser[userIp].getCountPacket() == 0)
                return false;

            //if number of packet greater then x in  y second - remove from white list
            if ((DateTime.Now - overLoadUser[userIp].getFirstTime()).TotalSeconds <= _conf.Y && overLoadUser[userIp].getCountPacket() >= _conf.X)
            {
                updateWhiteList(userIp);     //remove user from whiteFile
                overLoadUser.Remove(userIp); //remove user from request dict
                return true;
            }
            return false;

        }

        //delete user from white list and white file
        private void updateWhiteList(string userIp)
        {
            whiteList.Remove(userIp);
            File.Delete(this._conf.WhiteFile);
            foreach (string user in whiteList)
                this._fileReader.addEncryptedUser(user, _conf.WhiteFile);
        }

        public static void forbbiden(HttpListenerContext context)
        {

            if (preMade403Response == null)
            {
                // Set up an authentication error response template.
                context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
                context.Response.StatusDescription = "403 Forbidden";
                context.Response.ProtocolVersion = new Version("1.0");
                context.Response.SendChunked = false;
                preMade403Response = context.Response;
                Console.WriteLine("403: Forbidden");
            }
            else
            {
                context.Response.CopyFrom(preMade403Response);
            }

            // The response body cannot be saved in the template.

            StringBuilder message = new StringBuilder();
            message.Append("<HTML><BODY>");
            message.Append("<h1>403 Forbidden Error </h1><p> You don't have premission to access this page on this server. </p> ");
            message.Append("</BODY></HTML>");
            message403 = message.ToString();

            // Turn the error message into a byte array using the  
            // encoding from the response when present.
            System.Text.Encoding encoding = context.Response.ContentEncoding;
            if (encoding == null)
            {
                encoding = System.Text.Encoding.UTF8;
                context.Response.ContentEncoding = encoding;
            }

            byte[] buffer = encoding.GetBytes(message403);
            context.Response.ContentLength64 = buffer.Length;
            // Write the error message.
            System.IO.Stream stream = context.Response.OutputStream;
            stream.Write(buffer, 0, buffer.Length);
            // Send the response.
            context.Response.Close();
        }

        /// <summary>
        /// decides if to ask for Authentication from the user by checking the whitlist
        /// </summary>
        /// <param name="request"></param> the given request
        /// <returns></returns>
        private AuthenticationSchemes AuthenticationSchemeForClient(HttpListenerRequest request)
        {
            if (whiteList.Contains(request.RemoteEndPoint.Address.ToString()))
                return AuthenticationSchemes.Anonymous;
            else
                return AuthenticationSchemes.Basic;
        }

    }
}
